var __createBinding =
    (this && this.__createBinding) ||
    (Object.create
        ? (o, m, k, k2) => {
              if (k2 === undefined) k2 = k;
              var desc = Object.getOwnPropertyDescriptor(m, k);
              if (
                  !desc ||
                  ("get" in desc
                      ? !m.__esModule
                      : desc.writable || desc.configurable)
              ) {
                  desc = { enumerable: true, get: () => m[k] };
              }
              Object.defineProperty(o, k2, desc);
          }
        : (o, m, k, k2) => {
              if (k2 === undefined) k2 = k;
              o[k2] = m[k];
          });
var __setModuleDefault =
    (this && this.__setModuleDefault) ||
    (Object.create
        ? (o, v) => {
              Object.defineProperty(o, "default", {
                  enumerable: true,
                  value: v,
              });
          }
        : (o, v) => {
              o.default = v;
          });
var __importStar =
    (this && this.__importStar) ||
    ((mod) => {
        if (mod?.__esModule) return mod;
        var result = {};
        if (mod != null)
            for (var k in mod)
                if (k !== "default" && Object.hasOwn(mod, k))
                    __createBinding(result, mod, k);
        __setModuleDefault(result, mod);
        return result;
    });
var __importDefault =
    (this && this.__importDefault) ||
    ((mod) => (mod?.__esModule ? mod : { default: mod }));
Object.defineProperty(exports, "__esModule", { value: true });
exports.ControlFlowRecoverer = void 0;
const t = __importStar(require("@babel/types"));
const traverse_1 = __importDefault(require("@babel/traverse"));
const transformation_1 = require("../transformation");
const variable_1 = require("../../helpers/variable");
const declaration_1 = require("../../helpers/declaration");
class ControlFlowRecoverer extends transformation_1.Transformation {
    /**
     * Executes the transformation.
     * @param log The log function.
     */
    execute(_log) {
        const self = this;
        (0, traverse_1.default)(this.ast, {
            enter(path) {
                const stateVariable = (0, variable_1.findConstantVariable)(
                    path,
                    isStateArrayExpression,
                );
                if (!stateVariable) {
                    return;
                }
                const states =
                    stateVariable.expression.callee.object.value.split(
                        stateVariable.expression.arguments[0].value,
                    );
                const statementPath = path.getStatementParent();
                if (!statementPath) {
                    return;
                }
                let nextPath = statementPath.getNextSibling();
                let initialValue;
                if (isFlatteningForLoop(nextPath.node, stateVariable.name)) {
                    initialValue = t.isAssignmentExpression(nextPath.node.init)
                        ? nextPath.node.init.right.value
                        : nextPath.node.init.declarations[0].init.value;
                } else if (
                    (0, declaration_1.isDeclarationOrAssignmentExpression)(
                        nextPath.node,
                        t.isIdentifier,
                        t.isNumericLiteral,
                    )
                ) {
                    const counterName = t.isAssignmentExpression(nextPath.node)
                        ? nextPath.node.left.name
                        : nextPath.node.declarations[0].id.name;
                    initialValue = t.isAssignmentExpression(nextPath.node)
                        ? nextPath.node.right.value
                        : nextPath.node.declarations[0].init.value;
                    nextPath = nextPath.getNextSibling();
                    if (
                        !isFlatteningWhileLoop(
                            nextPath.node,
                            stateVariable.name,
                            counterName,
                        )
                    ) {
                        return;
                    }
                } else {
                    return;
                }
                const cases = nextPath.node.body.body[0].cases;
                const casesMap = new Map(
                    cases.map((c) => [c.test.value, c.consequent]),
                );
                const statements = [];
                for (let i = initialValue; ; i++) {
                    const state = states[i];
                    if (!casesMap.has(state)) {
                        break;
                    }
                    const blockStatements = casesMap.get(state);
                    statements.push(
                        ...blockStatements.filter(
                            (s) => !t.isContinueStatement(s),
                        ),
                    );
                    if (
                        blockStatements.length > 0 &&
                        t.isReturnStatement(
                            blockStatements[blockStatements.length - 1],
                        )
                    ) {
                        break;
                    }
                }
                path.remove();
                nextPath.replaceWithMultiple(statements);
                self.setChanged();
            },
        });
        return this.hasChanged();
    }
}
exports.ControlFlowRecoverer = ControlFlowRecoverer;
ControlFlowRecoverer.properties = {
    key: "controlFlowRecovery",
    rebuildScopeTree: true,
};
/**
 * Returns whether a node is a state array expression.
 * @param node The node.
 * @returns Whether.
 */
const isStateArrayExpression = (node) => {
    return (
        t.isCallExpression(node) &&
        t.isMemberExpression(node.callee) &&
        t.isStringLiteral(node.callee.object) &&
        ((t.isStringLiteral(node.callee.property) &&
            node.callee.property.value === "split") ||
            (t.isIdentifier(node.callee.property) &&
                node.callee.property.name === "split")) &&
        node.arguments.length === 1 &&
        t.isStringLiteral(node.arguments[0])
    );
};
/**
 * Returns whether a node is the body of a control flow flattening loop.
 * @param node The AST node.
 * @param statesName The name of the variable containing the states.
 * @param counterName The name of the block counter variable.
 * @returns Whether.
 */
const isFlatteningLoopBody = (node, statesName, counterName) => {
    return (
        t.isBlockStatement(node) &&
        node.body.length === 2 &&
        t.isBreakStatement(node.body[1]) &&
        t.isSwitchStatement(node.body[0]) &&
        t.isMemberExpression(node.body[0].discriminant) &&
        t.isIdentifier(node.body[0].discriminant.object) &&
        node.body[0].discriminant.object.name === statesName &&
        t.isUpdateExpression(node.body[0].discriminant.property) &&
        t.isIdentifier(node.body[0].discriminant.property.argument) &&
        node.body[0].discriminant.property.argument.name === counterName &&
        node.body[0].cases.every((c) => c.test && t.isStringLiteral(c.test))
    );
};
/**
 * Returns whether a node is a control flow flattening for loop.
 * @param node The node.
 * @param statesName The name of the variable containing the states.
 * @returns Whether.
 */
const isFlatteningForLoop = (node, statesName) => {
    return (
        t.isForStatement(node) &&
        node.init !== undefined &&
        (0, declaration_1.isDeclarationOrAssignmentExpression)(
            node.init,
            t.isIdentifier,
            t.isNumericLiteral,
        ) &&
        isFlatteningLoopBody(
            node.body,
            statesName,
            t.isAssignmentExpression(node.init)
                ? node.init.left.name
                : node.init.declarations[0].id.name,
        )
    );
};
/**
 * Returns whether a node is a control flow flattening while loop.
 * @param node The node.
 * @param statesName The name of the variable containing the states.
 * @returns Whether.
 */
const isFlatteningWhileLoop = (node, statesName, counterName) => {
    return (
        t.isWhileStatement(node) &&
        t.isBooleanLiteral(node.test) &&
        node.test.value === true &&
        isFlatteningLoopBody(node.body, statesName, counterName)
    );
};
